| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112 |
- import { NextRequest, NextResponse } from 'next/server';
- import { LoginResponse } from '@/types/response/auth';
- import { fetchJson } from '@/lib/utils/server';
- import { isSocialLoginProvider, SOCIAL_PROVIDER_LABELS, OAUTH_STATE_COOKIE_PREFIX } from '@/constants/social';
- /**
- * 소셜 로그인 콜백 라우트 (Naver/Kakao) — /login/{provider}/callback
- *
- * 1. ?code&state(&error) 수신 → start 라우트가 심은 httpOnly state 쿠키와 대조 (CSRF 방어)
- * 2. 백엔드 POST /api/auth/social-login (code 교환 + userinfo + 계정 매칭)
- * 3. 성공: accessToken/refreshToken httpOnly 쿠키(기존 google/callback 옵션 동일) → /login/{provider}/complete
- * 4. Auth.EmailRequired / Auth.AccountExists: 안내 화면(/login/social-notice)으로 분기
- * 5. 기타 실패: /login?error={message} (로그인 페이지 alert 관례)
- */
- export async function GET(request: NextRequest, { params }: { params: Promise<{ provider: string }> })
- {
- const { provider } = await params;
- if (!isSocialLoginProvider(provider)) {
- const url = new URL('/login', request.url);
- url.searchParams.set('error', '지원하지 않는 소셜 로그인입니다.');
- return NextResponse.redirect(url);
- }
- const label = SOCIAL_PROVIDER_LABELS[provider];
- const stateCookieName = `${OAUTH_STATE_COOKIE_PREFIX}${provider}`;
- const savedState = request.cookies.get(stateCookieName)?.value ?? null;
- // state 쿠키는 일회용 — 성공/실패와 무관하게 응답에서 제거
- const finish = (response: NextResponse) => {
- response.cookies.delete(stateCookieName);
- return response;
- };
- const failRedirect = (message: string) => {
- const url = new URL('/login', request.url);
- url.searchParams.set('error', message);
- return finish(NextResponse.redirect(url));
- };
- const noticeRedirect = (reason: 'email-required'|'account-exists') => {
- const url = new URL('/login/social-notice', request.url);
- url.searchParams.set('reason', reason);
- url.searchParams.set('provider', provider);
- return finish(NextResponse.redirect(url));
- };
- const searchParams = request.nextUrl.searchParams;
- const code = searchParams.get('code');
- const state = searchParams.get('state');
- const oauthError = searchParams.get('error');
- const oauthErrorDescription = searchParams.get('error_description');
- // 사용자가 동의 화면에서 취소한 경우 등 provider 측 에러
- if (oauthError) {
- if (oauthError === 'access_denied') {
- return failRedirect(`${label} 로그인이 취소되었습니다.`);
- }
- return failRedirect(oauthErrorDescription || `${label} 인증에 실패했습니다. 다시 시도해주세요.`);
- }
- if (!code) {
- return failRedirect(`${label} 인증 코드가 없습니다. 다시 시도해주세요.`);
- }
- if (!state || !savedState || state !== savedState) {
- return failRedirect('로그인 요청 검증(state)에 실패했습니다. 다시 시도해주세요.');
- }
- try {
- const redirectUri = `${request.nextUrl.origin}/login/${provider}/callback`;
- const res = await fetchJson<LoginResponse>('/api/auth/social-login', {
- method: 'POST',
- body: JSON.stringify({
- Provider: provider,
- Code: code,
- State: state,
- RedirectUri: redirectUri
- })
- });
- if (res.success && res.data) {
- const response = NextResponse.redirect(new URL(`/login/${provider}/complete`, request.url));
- const cookieOptions = { httpOnly: true, path: '/' };
- response.cookies.set('accessToken', res.data.accessToken, cookieOptions);
- response.cookies.set('refreshToken', res.data.refreshToken, cookieOptions);
- return finish(response);
- }
- const errorCode = res.errors?.[0]?.code;
- // 카카오 이메일 선택동의 미동의 → 이메일 제공 동의 안내 화면
- if (errorCode === 'Auth.EmailRequired') {
- return noticeRedirect('email-required');
- }
- // 같은 이메일의 기존 계정 존재 (자동연결 금지) → 기존 계정 로그인 + SNS 연동 안내 화면
- if (errorCode === 'Auth.AccountExists') {
- return noticeRedirect('account-exists');
- }
- if (errorCode === 'Auth.SocialNotConfigured') {
- return failRedirect(`${label} 로그인이 아직 준비되지 않았습니다. 잠시 후 다시 시도해주세요.`);
- }
- return failRedirect(res.errors?.[0]?.description || res.message || `${label} 로그인에 실패했습니다. 다시 시도해주세요.`);
- } catch (e) {
- console.error(`[${provider} Callback] Error:`, e);
- return failRedirect(e instanceof Error ? e.message : `${label} 로그인 처리 중 오류가 발생했습니다.`);
- }
- }
|